//----------------------------------------------------------------------------
//  ,-.
// /_\___ ,   Ninja Framework
// '  ___\    (C) Riccardo Capra - http://riccar.do
//  //   \\
//----------------------------------------------------------------------------
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
//----------------------------------------------------------------------------

#include "NjExample01.h"

#if NJ_PLATFORM_OS_LINUX
#	include "NjEventsSystemLinux.h"
#	include "NjPlacementNew.h"

	//----------------------------------------------------------------------------
	class NjEventsSystemLinux::Static
	{
	public:
		static NjUint const DEFAULT_SPIN_COUNT = 4096;

		NJ_DISABLE_INSTANCE(Static);
	};
	//----------------------------------------------------------------------------
	class NjEventsSystemLinux::EventImpl
	{
	public:
		EventImpl(NjBool bSet);
		~EventImpl();

		TryResult TryWait(TimeMs uTimeMs);
		NjBool Set();
		NjBool Unset();
		NjBool Destroy();

	private:
		typedef NjUint32 Futex;
		Futex volatile m_uFutex;
#	if NJ_PLATFORM_64_BIT
		NjUint8 m_aPadding01[sizeof(NjAtomicStack::Node)-sizeof(Futex)];
#	endif // NJ_PLATFORM_64_BIT

		static const Futex FUTEX_EVENT_SET           = 0x80000000;
		static const Futex FUTEX_WAITING_THREAD_ONE  = 0x00000001;
		static const Futex FUTEX_WAITING_THREAD_MASK = 0x7FFFFFFF;

		static void ToLinuxTime(TimeMs uTimeMs, timespec & kTime);

		NJ_DISABLE_COPY(EventImpl);
	};
	//----------------------------------------------------------------------------
	NjEventsSystemLinux::NjEventsSystemLinux()
#if NJ_ASSERT_ENABLED
	:
		m_kTotEvents(0)
#endif // NJ_ASSERT_ENABLED
	{
		if (sysconf(_SC_NPROCESSORS_ONLN)<=1)
			m_uSpinCount=1;
		else
			m_uSpinCount=Static::DEFAULT_SPIN_COUNT;
	}
	//----------------------------------------------------------------------------
	NjEventsSystemLinux::~NjEventsSystemLinux()
	{
		FlushCache();
		NJ_ASSERT(m_kTotEvents.Get()==0);
	}
	//----------------------------------------------------------------------------
	void NjEventsSystemLinux::FlushCache()
	{
		NjAtomicStack::FlushNode * pFlushNode(m_kFreeEvents.Flush());

		while (pFlushNode!=NJ_NULL)
		{
			NjAtomicStack::Node * pNode(pFlushNode->GetNode());
			pFlushNode=pFlushNode->GetNext();

			pNode->~Node();
			NjPlacementNew kPlacement(pNode);
			NjDelete(NJ_PLACEMENT_NEW(kPlacement, EventImpl(NJ_FALSE)));

			NJ_ASSERT(m_kTotEvents.PostDecrement()>0);
		}
	}
	//----------------------------------------------------------------------------
	NjEventsSystemLinux::Event * NjEventsSystemLinux::CreateEvent(NjBool bSet)
	{
		NJ_COMPILE_ASSERT(sizeof(NjAtomicStack::Node)<=sizeof(EventImpl));
		NJ_ASSERT(IsImplementationSet());

		NjAtomicStack::Node * const pNode(m_kFreeEvents.Pop());
		EventImpl * pEventImpl;

		if (pNode==NJ_NULL)
		{
			NJ_ASSERT(m_kTotEvents.PreIncrement()>0);
			pEventImpl=NjNew(EventImpl(bSet));
		}
		else
		{
			pNode->~Node();
			NjPlacementNew kPlacement(pNode);
			pEventImpl=NJ_PLACEMENT_NEW(kPlacement, EventImpl(bSet));
		}

		return reinterpret_cast<Event *>(pEventImpl);
	}
	//----------------------------------------------------------------------------
	NjEventsSystemLinux::TryResult NjEventsSystemLinux::TryWaitEvent(Event * pEvent, TimeMs uTimeMs)
	{
		NJ_ASSERT(IsImplementationSet());

		EventImpl * const pEventImpl(reinterpret_cast<EventImpl *>(pEvent));

		if (pEventImpl==NJ_NULL)
			return NjEventsSystem::TRY_FAILURE;

		return pEventImpl->TryWait(uTimeMs);
	}
	//----------------------------------------------------------------------------
	NjBool NjEventsSystemLinux::SetEvent(Event * pEvent)
	{
		NJ_ASSERT(IsImplementationSet());

		EventImpl * const pEventImpl(reinterpret_cast<EventImpl *>(pEvent));

		if (pEventImpl==NJ_NULL)
			return NJ_FALSE;

		return pEventImpl->Set();
	}
	//----------------------------------------------------------------------------
	NjBool NjEventsSystemLinux::UnsetEvent(Event * pEvent)
	{
		NJ_ASSERT(IsImplementationSet());

		EventImpl * const pEventImpl(reinterpret_cast<EventImpl *>(pEvent));

		if (pEventImpl==NJ_NULL)
			return NJ_FALSE;

		return pEventImpl->Unset();
	}
	//----------------------------------------------------------------------------
	NjBool NjEventsSystemLinux::DestroyEvent(Event * pEvent)
	{
		NJ_ASSERT(IsImplementationSet());

		EventImpl * const pEventImpl(reinterpret_cast<EventImpl *>(pEvent));

		if (pEventImpl==NJ_NULL)
			return NJ_FALSE;

		if (!pEventImpl->Destroy())
		{
			NJ_BREAK();
			return NJ_TRUE;
		}
		
		pEventImpl->~EventImpl();
		NjPlacementNew kPlacement(pEventImpl);
		m_kFreeEvents.Push(NJ_PLACEMENT_NEW(kPlacement, NjAtomicStack::Node));
		return NJ_TRUE;
	}
	//----------------------------------------------------------------------------
	NjBool NjEventsSystemLinux::GetCurrentThreadDebugId(NjUint & uDebugId)
	{
		NJ_ASSERT(IsImplementationSet());

		uDebugId=NjUint(pthread_self());
		return NJ_TRUE;
	}
	//----------------------------------------------------------------------------
	NjBool NjEventsSystemLinux::GetTimeMs(TimeMs & uTimeMs)
	{
		timeval kNow;
		if (gettimeofday(&kNow, NJ_NULL)!=0)
			return NJ_FALSE;

		uTimeMs=TimeMs(kNow.tv_sec)*1000+TimeMs(kNow.tv_usec)/1000;
		return NJ_TRUE;
	}
	//----------------------------------------------------------------------------
	NjBool NjEventsSystemLinux::GetSpinCount(NjUint & uSpinCount)
	{
		uSpinCount=m_uSpinCount;
		return NJ_TRUE;
	}
	//----------------------------------------------------------------------------
	inline NjEventsSystemLinux::EventImpl::EventImpl(NjBool bSet)
	:
		m_uFutex(bSet ? FUTEX_EVENT_SET : 0)
	{
	}
	//----------------------------------------------------------------------------
	inline NjEventsSystemLinux::EventImpl::~EventImpl()
	{
		// Ensures there are no waiting threads
		NJ_ASSERT((m_uFutex&FUTEX_WAITING_THREAD_MASK)==0);
	}
	//----------------------------------------------------------------------------
	inline NjEventsSystemLinux::TryResult NjEventsSystemLinux::EventImpl::TryWait(TimeMs uTimeMs)
	{
		Futex uFutex(m_uFutex);
		timespec kTimeOut;
		timespec * pTimeOut(NJ_NULL);
		
		for (;;)
		{
			for (;;)
			{
				// It's set. There is no need to wait
				if ((uFutex&FUTEX_EVENT_SET)!=0)
					return NjEventsSystem::TRY_SUCCESS;
			
				NJ_ASSERT((uFutex&FUTEX_WAITING_THREAD_MASK)!=FUTEX_WAITING_THREAD_MASK);
			
				// Add a further waiting thread
				Futex const uNewFutex(uFutex+FUTEX_WAITING_THREAD_ONE);
				Futex const uOldFutex(__sync_val_compare_and_swap(&m_uFutex, uFutex, uNewFutex));
			
				if (uOldFutex==uFutex)
					break;
			
				uFutex=uOldFutex;
			}
		
			if (uTimeMs!=NjEventsSystem::WAIT_FOREVER)
			{
				pTimeOut=&kTimeOut;
				ToLinuxTime(uTimeMs, kTimeOut);
				uTimeMs=NjEventsSystem::WAIT_FOREVER;
			}
		
			int const iResult(syscall(SYS_futex, &m_uFutex, FUTEX_WAIT_PRIVATE, uFutex+FUTEX_WAITING_THREAD_ONE, pTimeOut, NJ_NULL, 0));

			uFutex=__sync_fetch_and_sub(&m_uFutex, FUTEX_WAITING_THREAD_ONE);
			NJ_ASSERT((uFutex&FUTEX_WAITING_THREAD_MASK)!=0);
		
			if (iResult==0) // FUTEX_WAKE has been called
				return NjEventsSystem::TRY_SUCCESS;

			int const iErrno(errno);
			if (iErrno==EWOULDBLOCK) // m_uFutex did change
				continue;
				
			if (errno==ETIMEDOUT) // Timeout
				return NjEventsSystem::TRY_TIMEOUT;

			NJ_BREAK();
			// Other errors including EINTR
			return NjEventsSystem::TRY_FAILURE;
		}
	}
	//----------------------------------------------------------------------------
	NjBool NjEventsSystemLinux::EventImpl::Set()
	{
		Futex uFutex(__sync_fetch_and_or(&m_uFutex, FUTEX_EVENT_SET));
		
		if ((uFutex&FUTEX_EVENT_SET)!=0)
			return NJ_TRUE;
				
		for(;;)
		{
			Futex const uToWakenThreads((uFutex&FUTEX_WAITING_THREAD_MASK)/FUTEX_WAITING_THREAD_ONE);
			if (uToWakenThreads==0)
				return NJ_TRUE;
		
			syscall(SYS_futex, &m_uFutex, FUTEX_WAKE_PRIVATE, uToWakenThreads, NJ_NULL, NJ_NULL, 0);
			uFutex=m_uFutex;

			if ((uFutex&FUTEX_EVENT_SET)==0)
				return NJ_TRUE;
		}
	}
	//----------------------------------------------------------------------------
	NjBool NjEventsSystemLinux::EventImpl::Unset()
	{
		__sync_fetch_and_and(&m_uFutex, ~FUTEX_EVENT_SET);
		return NJ_TRUE;
	}
	//----------------------------------------------------------------------------
	inline NjBool NjEventsSystemLinux::EventImpl::Destroy()
	{
		return (m_uFutex&FUTEX_WAITING_THREAD_MASK)==0;
	}
	//----------------------------------------------------------------------------
	inline void NjEventsSystemLinux::EventImpl::ToLinuxTime(TimeMs uTimeMs, timespec & kTime)
	{
		kTime.tv_sec=time_t(uTimeMs/1000);
		kTime.tv_nsec=long(uTimeMs%1000)*1000000;
	}
	//----------------------------------------------------------------------------

#endif // NJ_PLATFORM_OS_LINUX
